
import java.util.*;
import java.net.*;
import java.io.*;

import org.newdawn.slick.*;

/**
* Each client that want to play must connect to a GameServer. If the user just
* wants to play with himself, then a local GameServer have to be created (with
* maximum number of players set to 1).
*
* The network protocol:
*	All messages sent to the server must end with an new line.
*	All messages sent from the server will end with a new line.
* From server:
*		id <id>
*			- The first response from the server when a client connects is this
*			  where <id> is the id that the client got. This is used to
*			  represent the client when it issues commands or chats. The client
*			  should send its player name directly afterwards.
*		gamestarted <seed>
*			- This is sent whenever the host issues the 'start' command, it should
*			  activate the actual gameplay on every client. <seed> is the random
*			  seed that all clients should use to random new pieces and such.
*		newclient <id> <color red> <color blue> <color green> <observer state> <name>
*			- For every new client that connects, this will be sent everyone
*			  connected, even the client that just connected. The client that just
*			  connected will recieve one of these per client that is connected.
*			  The colors are specified using floating values.
*		info <id> <observer state> <name>
*			- Sets some states of a client. Can only be used when the game hasn't
*			  started yet.
*		chat <id> <message>
*			- All chat messages sent to the server will be relayed to every client
*			  including the client that sent it. <id> represents which client that
*			  sent it.
*		pulse <timestamp> <number of actions> [<actions>]
*			- After the 'gamestarted' message, these pulses will be sent in a
*			  constant interval. <timestamp> should start at 0 and be incremented
*			  by 1 each pulse. If it doesn't, some form of desynchronization have
*			  happened. <number of actions> says how many actions that follow.
*			<actions> appear on a new line each (if no actions, there will be no
*			  new line) in the format:
*				<client id> <action>
*			Example #1:
*				pulse 12 2
*				0 move_right
*				1 rotate_left
*			Example #2:
*				pulse 14 0
*		clientdisconnected <id>
*			- When a client is disconnected, this message will be sent.
*		serverfull
*			- When a client tries to connect, but the number of players connected
*			  have reached its limit.
*		pong <parameters>
*			- Sent whenever the cliet says ping, the parameters are the same as the
*			  client sent.
*		servershutdown
*			- When the server is shut down, this message is sent to everyone.
*		playeridswitch <id> <replaced id>
*			- When a bot replaces a dropped player, or when an observer player replaces
*			  a bot. <id> represents the player/bot that will replace <replaced id>.
*		rematch
*			- Sent when the host have requested a rematch. Everyone's game should be
*			  switched to the game lobby.
*
* To server:
*		start
*			- Host only command. This will send out a 'gamestarted' command to all
*			  connected clients, and begin to send out pulses.
*		chat <message>
*			- Sends a chat message to all other connected clients.
*		name <name>
*			- Sets the client's name to <name>. Should only be sent directly
*			  when connected (after the server sent the clients id). The server will
*			  respond by sending newclient-messages for every client that is already
*			  connected.
*		info <observer state> <name>
*			- Sets some of this clients states to the given values. Can only be used
*			  if the game hasn't started.
*		cmd <action>
*			- A game action. The action is put in the action queue, and will be
*			  sent to all clients at the same time and order as they arrived to
*			  the GameServer.
*		disconnect
*			- Disconnects the client. Sends a 'clientdisconnected' message to all
*			  connected clients.
*		ping [<parameters>]
*			- Responds with pong directly, the parameters are just forwarded with
*			  the pong. The parameters can be used for things such as measuring the
*			  delay to the server. Note, this message will not go through the action
*			  queue that the game actions are. The parameter can be anything, and is
*			  optional.
*		playeridswitch <id>
*			- Notifies the server that the client (bot or player) wants to replace
*			  the player with id <id>.
*		rematch
*			- If the game have started, then the server will try to restart it and
*			  send everyone back to the lobby.
*/
public class GameServer implements Runnable {
	/**
	* ServerSocket which listens for new clients
	*/
	private ServerSocket serverSocket;
	/**
	* The next timestamp to be used for pulsing. This value is incrememented by
	* 1 each pulse
	*/
	private int timeStamp;
	/**
	* Maximum number of players that the server will allow to be connected
	* at any given time.
	*/
	private int maximumNumberOfPlayers;
	private boolean gameStarted;
	private long lastPulse;
	private long pulseInterval;
	private long clientTimeoutDelay = 10000;
	private long lastRefresh;
	private long refreshInterval = 20000;
	private boolean allowObservers;

	private LinkedList<String> actions;
	private LinkedList<EasySocket> sockets;
	private HashMap<EasySocket, Integer> socketIds;
	private HashMap<EasySocket, String> socketNames;
	private HashMap<EasySocket, Long> socketTimeouts;
	private HashMap<EasySocket, Color> socketColors;
	private HashMap<EasySocket, Boolean> socketObserving;
	private int nextPlayerId = 0;

	private LinkedList<Tuple3<EasySocket, Long, String>> partStates = new LinkedList<Tuple3<EasySocket, Long, String>>();

	private EasySocket host = null;

	private Thread listenThread;
	private GameServerMainLoop gsMainLoop;
	private boolean serverStopped = false;

	private boolean multiplayer = true;
	
	private final static Color[] PLAYER_COLORS = {new Color(255,0,0),new Color(0,255,0),new Color(30,70,255), new Color(255,255,0), new Color(0,255,255), new Color(255,0,255), new Color(200,100,0), new Color(255,255,255), new Color(150,150,150)};//, new Color(50, 50, 50)};


	/**
	* Starts a new gameserver.
	*
	* @param maximumNumberOfPlayers maximum number of allowed players simultaneous
	* @param pulseInterval defines the interval between every pulse that is sent to the
	*						when the game have started. Shorter interval makes the game
	*						more responsive, but takes more bandwidth. It is measured in
	*						milliseconds.
	* @param port the port which the server listens to for new connections
	*/
	public GameServer(int pulseInterval, int port) {
		this(1, pulseInterval, port, false);
		multiplayer = false;
	}

	public GameServer(int maximumNumberOfPlayers, int pulseInterval, int port) {
		this(maximumNumberOfPlayers, pulseInterval, port, true);
	}
	
	public GameServer(int maximumNumberOfPlayers, int pulseInterval, int port, boolean allowObservers) {
		this.maximumNumberOfPlayers = maximumNumberOfPlayers;
		this.pulseInterval = pulseInterval;
		this.allowObservers = allowObservers;

		actions = new LinkedList<String>();
		sockets = new LinkedList<EasySocket>();
		socketIds = new HashMap<EasySocket, Integer>();
		socketNames = new HashMap<EasySocket, String>();
		socketTimeouts = new HashMap<EasySocket, Long>();
		socketColors = new HashMap<EasySocket, Color>();
		socketObserving = new HashMap<EasySocket, Boolean>();

		try {
			serverSocket = new ServerSocket(port);
		} catch (IOException e) {
			e.printStackTrace();
			System.exit(1);
		}
		listenThread = new Thread(this);
		listenThread.start();
		gsMainLoop = new GameServerMainLoop(this);
		gsMainLoop.start();
	}

	/**
	* The loop recieving and sending packages to all connected clients. Also
	* sends pulses in a constant interval.
	*/
	public void mainLoop() {
		LinkedList<EasySocket> deadSockets = new LinkedList<EasySocket>();
		while (!serverStopped) {
			synchronized (sockets) {
				if (gameStarted && System.nanoTime() > lastPulse + pulseInterval*1000000) {
					lastPulse += pulseInterval*1000000;
					sendPulse();
				}
				
				if (multiplayer && gameStarted && System.nanoTime() > lastRefresh + refreshInterval*1000000) {
					lastRefresh += refreshInterval*1000000;
					Multitris.clientCommunication.refreshAddedServer();
				}

				Iterator<Tuple3<EasySocket, Long, String>> it = partStates.iterator();
				while (it.hasNext()) {
					Tuple3<EasySocket, Long, String> part = it.next();
					if (part.second < System.nanoTime()) {
						part.second = System.nanoTime() + 1000000l*200l; // +0.25 seconds
						int len = part.third.length();
						int blockLength = 1000;
						if (len > blockLength) {
							sendMsg(part.first, "statepart "+part.third.substring(0, blockLength));
							part.third = part.third.substring(blockLength);
						} else {
							sendMsg(part.first, "statepart "+part.third);
							sendMsg(part.first, "statefinished");
							it.remove();
						}
					}
				}
				
				for (EasySocket s : sockets) {
					try {
						if (s.getSocket().isClosed() || 
							!s.getSocket().isConnected() || 
							s.getLastException() != null || 
							socketTimeouts.get(s) < System.nanoTime() - 1000000l*clientTimeoutDelay) {
							deadSockets.add(s);
							continue;
						}
						//if (s.getInputStream().available() > 0) {
						if (s.hasNextLine()) {
							socketTimeouts.put(s, System.nanoTime());
							//Scanner in = new Scanner(s.getInputStream());
							String line = s.nextLine(); //in.nextLine();
							if (line.length()<500) {
								System.out.println("In: "+line);
							} else {
								System.out.println("In: "+line.substring(0,500)+" ...");
							}
							Integer id = socketIds.get(s);

							if (s == host) {
								if (line.equals("stop")) {
									stop();
								} else if (line.equals("start")) {
									start();
								} else if (line.equals("rematch") && gameStarted) {
									rematch();
								}
							}
							if (line.startsWith("name ")) {
								String[] parts = line.split(" ", 2);
								String name = parts[1];
								socketNames.put(s, name);
								socketObserving.put(s, gameStarted);

								ArrayList<Color> newColor = new ArrayList<Color>();
								for (int i=0; i<PLAYER_COLORS.length; i++) {
									newColor.add(PLAYER_COLORS[i]);
								}
								Set<Map.Entry<EasySocket, Color>> entries = socketColors.entrySet();
								for (Map.Entry<EasySocket, Color> entry : entries) {
									newColor.remove(entry.getValue());
								}
								
								if (newColor.size() == 0) { // all predefined colors are set
									socketColors.put(s, new Color((float)Math.random()*0.5f+0.5f, (float)Math.random()*0.5f+0.5f, (float)Math.random()*0.5f+0.5f));
								} else { // there are some colors left, take the first one
									socketColors.put(s, newColor.get(0)); 
								}


								for (EasySocket client : sockets) {
									if (client != s) {
										String clientName = socketNames.get(client);
										int clientId = socketIds.get(client);
										Color c = socketColors.get(client);
										int obs = (socketObserving.get(client) ? 1 : 0);
										if (clientName == null) clientName = "unknown";
										sendMsg(s, "newclient " + clientId + " " + 
											c.r + " " + c.g + " " + c.b + " " + obs + " " + 
											clientName);
									}
								}
								Color c = socketColors.get(s);
								sendMsgToAll("newclient " + id + " " + 
											c.r + " " + c.g + " " + c.b + " " + (gameStarted ? 1 : 0) + " " + 
											name);

								//if (gameStarted) {
								//	sendMsg(s, "sendstate " + id);
								//}
							} else if (line.startsWith("chat ")) {
								String[] parts = line.split(" ", 2);
								String msg = parts[1];
								sendMsgToAll("chat "+id+" "+msg);
							} else if (line.startsWith("info ")) {
								String[] parts = line.split(" ", 3);
								Boolean obs = parts[1].equals("1");
								String name = parts[2];
								socketNames.put(s, name);
								socketObserving.put(s, obs);
								sendMsgToAll("info "+id+" "+(obs ? "1" : "0")+" "+name);
							} else if (line.startsWith("cmd ")) {
								String[] parts = line.split(" ", 2);
								String cmd = parts[1];
								actions.add(id + " " + cmd);
							} else if (line.startsWith("state ")) { // a whole game state sent by the host to an observer
								String[] parts = line.split(" ", 3);
								int observerId = Integer.parseInt(parts[1]);
								for (Map.Entry<EasySocket, Integer> set : socketIds.entrySet()) {
									if (set.getValue() == observerId) {
										//sendMsg(set.getKey(), "state " + parts[2]);
										partStates.add(new Tuple3(set.getKey(), 0l, parts[2]));
										sendMsg(set.getKey(), "statestart " + parts[2].length());
										System.out.println("Sending state to "+observerId);
										break;
									}
								}
								System.out.println("End of state");
							} else if (line.equals("disconnect")) {
								deadSockets.add(s);
							} else if (line.startsWith("ping")) {
								sendMsg(s, "pong"+line.substring(4));
							} else if (line.startsWith("playeridswitch")) {
								String[] parts = line.split(" ", 2);
								sendMsgToAll("playeridswitch "+id+" "+parts[1]);
							}
						}
					//} catch (IOException e) {
					//	System.out.println("Error: IOException");
					//	e.printStackTrace();
					} catch (NumberFormatException e) {
						System.out.println("Error: Message from client was broken.");
						e.printStackTrace();
					} catch (IndexOutOfBoundsException e) {
						System.out.println("Error: Message from client didn't have enough parameters.");
						e.printStackTrace();
					}
				}
				for (EasySocket s : deadSockets) {
					System.out.println("Client disconnected");
					sendMsgToAll("clientdisconnected " + socketIds.get(s));
				}
				for (EasySocket s : deadSockets) {
					s.close();
					sockets.remove(s);
					socketIds.remove(s);
					socketColors.remove(s);
					socketNames.remove(s);
					socketTimeouts.remove(s);
					socketObserving.remove(s);
					Iterator<Tuple3<EasySocket, Long, String>> itDead = partStates.iterator();
					while (itDead.hasNext()) {
						Tuple3<EasySocket, Long, String> part = itDead.next();
						if (part.first == s)
							itDead.remove();
					}

					if (s == host) {
						stop();
					}
				}
				deadSockets.clear();
			}


			try {
				Thread.sleep(1);
			} catch (InterruptedException e) {
				System.err.println("Sleep interrupted in mainLoop() (GameServer)");
			}

		}
	}

	/**
	* Collects all queued actions and sends them to all clients together with
	* a timestamp. The timestamp is then incremented.
	*/
	private void sendPulse() {
		synchronized (actions) {
			String str = "pulse "+timeStamp+" "+actions.size();
			for (String action : actions) {
				str += "\n" + action;
			}
			actions.clear();
			sendMsgToAll(str);
		}

		timeStamp++;
	}
	
	/**
	* Sends a message to all connected clients.
	*
	* @param msg message to send
	*/
	private void sendMsgToAll(String msg) {
		for (EasySocket s : sockets) {
			if (!s.getSocket().isClosed() && s.getSocket().isConnected()) {
				sendMsg(s, msg);
			}
		}
	}

	/**
	* Sends a message to a specific client.
	*
	* @param socket socket to send the message to
	* @param msg message to send
	*/
	private void sendMsg(EasySocket socket, String msg) {
		socket.println(msg);
	}

	/**
	* This method will listen for new clients. It is started in a new thread so it
	* won't interrupt the program flow.
	*/
	public void run() {
		try {
			while (true) {
				EasySocket newClient = new EasySocket(serverSocket.accept());
				if (sockets.size() < maximumNumberOfPlayers) {
					newClient.getSocket().setKeepAlive(true);
					if (host == null) {
						host = newClient;
					}
					sendMsg(newClient, "id "+nextPlayerId);
					synchronized (sockets) {
						socketIds.put(newClient, nextPlayerId);
						sockets.add(newClient);
						socketTimeouts.put(newClient, System.nanoTime());
					}
					nextPlayerId++;
				} else {
					sendMsg(newClient, "serverfull");
					newClient.close();
				}
			}
		} catch (SocketException e) {
			//ServerSocket.accept() was interrupted
			System.out.println("SocketException");
		} catch (IOException e) {
			e.printStackTrace();
		}
		System.out.println("Server listen thread interrupted");
	}

	/**
	* Sends the 'gamestarted' message to all clients, and starts pulsing all incoming
	* actions.
	*/
	private void start() {
		System.out.println("Game started!");
		// If observers are allowed, do not close the server connection
		if (!allowObservers) {
			try {
				serverSocket.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}

		int seed = (int)(Math.random() * Integer.MAX_VALUE);
		synchronized (sockets) {
			sendMsgToAll("gamestarted "+seed);
		}
		actions.clear();
		timeStamp = 0;
		lastPulse = System.nanoTime();
		gameStarted = true;
		lastRefresh = System.nanoTime() - (refreshInterval - 5000) * 1000000;

		if (multiplayer) {
			Multitris.clientCommunication.setActiveOnCentralServer();
		}
	}

	private void rematch() {
		System.out.println("Game rematch initiated!");
		if (!allowObservers) {
			// If observers arent allow then the serverSocket was closed
			try {
				serverSocket = new ServerSocket(serverSocket.getLocalPort());
			} catch (SocketException e) {
			} catch (IOException e) {
			}
			listenThread.start();
		}
		gameStarted = false;

		sendMsgToAll("rematch");
	}

	private void stop() {
		System.out.println("Game stopped!");
		sendMsgToAll("servershutdown");
		gameStarted = false;
		serverStopped = true;
		try {
			serverSocket.close();
			for (EasySocket s : sockets) {
				s.getSocket().close();
			}
		} catch (IOException e) {
		}
		
		if (multiplayer) {
			Multitris.clientCommunication.removeFromCentralServer();
		}
	}



	public static void main(String args[]) {
		GameServer gs = new GameServer(20, 3000, 5398);
	}
}


class GameServerMainLoop extends Thread {
	GameServer gs;

	public GameServerMainLoop(GameServer gs) {
		this.gs = gs;
	}

	public void run() {
		gs.mainLoop();
	}
}